Example Data Analytics Carlos Mackenzie¶

Ejercicio 1¶

In [1]:
#1. Se cargan las librerias que se utilizaran para analizar los datos depositos_oink.csv
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import numpy as np
In [2]:
#2. Se carga el dataframe y se realiza una exploración inicial de los mismos:
df = pd.read_csv('D:/Mega/Personal/Varios/Procesos Seleccion/Coink/depositos_oinks.csv')
# para mayor facilidad, le cambio el nombre a la primera columna por "indice"
df.rename(columns={'Unnamed: 0':'indice',},inplace=True)
df
Out[2]:
indice user_id operation_value operation_date maplocation_name user_createddate
0 0 0e52d550-ae23-407a-9b1f-6e5fb1f066ab 273850.0 2022-01-14 13:07:56 CC Plaza de las Américas - Plaza Mariposa 2022-01-09 19:23:18.332689
1 1 975ed41e-d891-4c23-aeba-06363019d8e3 900.0 2022-02-06 13:11:25 CC Plaza de las Américas - Plaza Mariposa 2022-01-10 15:19:29.419075
2 2 f9043545-dfc2-402f-a8c7-0a61f21cb719 1800.0 2022-02-23 14:01:56 Universidad de los Andes - ML Piso 5 2021-08-25 12:44:48.524941
3 3 979ca8ad-9600-4a1f-81e9-e70c2f55cdc5 6500.0 2021-12-16 12:09:45 CC Plaza de las Américas - Plaza Mariposa 2021-07-17 17:11:16.766291
4 4 171db06e-2e4b-4542-a9c9-32028520fda4 150000.0 2022-01-25 12:00:18 CC Plaza de las Américas - Plaza Mariposa 2022-01-11 15:59:27.651994
... ... ... ... ... ... ...
4340 4340 c3d37131-76e8-4c4f-8e55-2778ab8817ad 850.0 2021-12-26 13:29:06 CC Los Molinos - Zona Montaña Nivel 2 2021-02-26 09:04:03.443362
4341 4341 40bc63c7-4ef1-420e-9e99-6c473b5fe5b1 1800.0 2022-01-23 14:32:37 CC Los Molinos - Zona Montaña Nivel 2 2022-01-23 14:31:03.394729
4342 4342 b99b3b89-8d45-4656-a950-c85f202897b1 3500.0 2021-12-06 19:33:24 CC Plaza de las Américas - Plaza Mariposa 2021-05-14 15:08:33.500127
4343 4343 443ffd46-0b2c-4383-9462-cf4b0519b6de 126200.0 2022-01-24 11:03:38 CC Los Molinos - Zona Montaña Nivel 2 2021-12-19 16:03:35.472917
4344 4344 0a8bbb74-a276-4434-895c-b30a94c04dee 100.0 2022-02-12 11:58:57 CC Plaza de las Américas - Plaza Mariposa 2022-02-12 11:51:40.981069

4345 rows × 6 columns

Exploración de los datos¶

In [3]:
#3 Se revisa el comportamiento de las diferentes variables
#3.1 user_id
res1=pd.DataFrame(df[['indice','user_id']].groupby('user_id').count())
res1.describe()
Out[3]:
indice
count 1656.000000
mean 2.623792
std 3.735081
min 1.000000
25% 1.000000
50% 1.000000
75% 3.000000
max 61.000000

Existen 1656 usuarios unicos, de los cuales el 50% solo han realizado 1 deposito, otro 25% de los usuarios han realizado de 2 a 3 operaciones y el 25% restante, mas de tres operaciones.

In [4]:
#3.2 operation value
df.hist('operation_value',bins=100,range=[0, 300000], align='mid')
plt.title("Histograma Variable Operation Value")
Out[4]:
Text(0.5, 1.0, 'Histograma Variable Operation Value')
In [5]:
df['op_ag']=pd.cut(df['operation_value'],bins=[0,5000,10000,15000,20000,25000,30000,35000,40000,45000,50000,1000000],
                   labels=['<5','5-10','10-15','15-20','20-25','25-30','30-35','35-40','40-45','45-50','>50'])
res2=pd.DataFrame(df[['indice','op_ag']].groupby('op_ag').count())
res2['par']=round(res2['indice']/sum(res2['indice'])*100,2)
res2['paracum']=res2['par'].cumsum()
plt.bar(res2.index,res2['par'])
plt.title('Participación de depósitos por rango')
plt.xlabel('Rango valor del depósito (Miles COP)')
plt.ylabel('% Participación')
res2
Out[5]:
indice par paracum
op_ag
<5 2085 48.09 48.09
5-10 612 14.11 62.20
10-15 294 6.78 68.98
15-20 193 4.45 73.43
20-25 136 3.14 76.57
25-30 95 2.19 78.76
30-35 89 2.05 80.81
35-40 66 1.52 82.33
40-45 58 1.34 83.67
45-50 68 1.57 85.24
>50 640 14.76 100.00

El 80% de los depósitos recibidos es menor a $ 35.000

In [6]:
# 3.3 operation date
# Inicialmente se generan a partir del operation_date, los campor anho_op, mes_op, dia_mes_op, dia_sem_op, hora_op y periodo_op
# se revisa el comportamiento de las operaciones para cada uno de esos valores
df['anho_op']=pd.DatetimeIndex(df['operation_date']).year
df['anho_op']=pd.to_numeric(df['anho_op'])
df['mes_op']=pd.DatetimeIndex(df['operation_date']).month.astype(str)
df['mes_op']=df['mes_op'].str.zfill(2)
df['periodo_op']=df['anho_op'].astype(str) + df['mes_op']
df['fecha']=pd.DatetimeIndex(df['operation_date']).date
df['dia_mes_op']=pd.DatetimeIndex(df['operation_date']).day
df['dia_semana_op']=pd.DatetimeIndex(df['operation_date']).weekday
df['hora_op']=pd.DatetimeIndex(df['operation_date']).hour
df['semana_op']=pd.DatetimeIndex(df['operation_date']).strftime("%W")
df['semana_op']=pd.to_numeric(df['semana_op'])
df['semana_op_a']=(df['anho_op']-2021)*52+df['semana_op']-46
df['fecha_cre_us']=pd.DatetimeIndex(df['user_createddate']).date
df.head()
Out[6]:
indice user_id operation_value operation_date maplocation_name user_createddate op_ag anho_op mes_op periodo_op fecha dia_mes_op dia_semana_op hora_op semana_op semana_op_a fecha_cre_us
0 0 0e52d550-ae23-407a-9b1f-6e5fb1f066ab 273850.0 2022-01-14 13:07:56 CC Plaza de las Américas - Plaza Mariposa 2022-01-09 19:23:18.332689 >50 2022 01 202201 2022-01-14 14 4 13 2 8 2022-01-09
1 1 975ed41e-d891-4c23-aeba-06363019d8e3 900.0 2022-02-06 13:11:25 CC Plaza de las Américas - Plaza Mariposa 2022-01-10 15:19:29.419075 <5 2022 02 202202 2022-02-06 6 6 13 5 11 2022-01-10
2 2 f9043545-dfc2-402f-a8c7-0a61f21cb719 1800.0 2022-02-23 14:01:56 Universidad de los Andes - ML Piso 5 2021-08-25 12:44:48.524941 <5 2022 02 202202 2022-02-23 23 2 14 8 14 2021-08-25
3 3 979ca8ad-9600-4a1f-81e9-e70c2f55cdc5 6500.0 2021-12-16 12:09:45 CC Plaza de las Américas - Plaza Mariposa 2021-07-17 17:11:16.766291 5-10 2021 12 202112 2021-12-16 16 3 12 50 4 2021-07-17
4 4 171db06e-2e4b-4542-a9c9-32028520fda4 150000.0 2022-01-25 12:00:18 CC Plaza de las Américas - Plaza Mariposa 2022-01-11 15:59:27.651994 >50 2022 01 202201 2022-01-25 25 1 12 4 10 2022-01-11
In [7]:
resanho1=pd.DataFrame(df[['indice','anho_op']].groupby('anho_op').count())
resanho2=pd.DataFrame(df[['operation_value','anho_op']].groupby('anho_op').sum())
resanho=pd.DataFrame(resanho1.merge(resanho2, left_on='anho_op', right_on='anho_op'))
resanho
Out[7]:
indice operation_value
anho_op
2021 1477 51195450.0
2022 2868 99221300.0
In [8]:
ganho, axs = plt.subplots(1, 2, figsize=(9, 3), sharey=False)
plt.subplots_adjust(left=0.1,bottom=0.1,right=0.9,top=0.8, wspace=0.4, hspace=0.4)
ganho.suptitle('Comportamiento por año',color='blue', fontsize=15)
axs[0].set_xticks([2021,2022])
axs[0].set_title('Número de Depositos',fontsize=10)
axs[0].bar(resanho.index, resanho.indice)
axs[1].set_xticks([2021,2022])
axs[1].set_title('Valor de Depositos',fontsize=10)
axs[1].bar(resanho.index, resanho.operation_value)
axs[1].set_yticks(ticks=plt.yticks()[0], labels=plt.yticks()[0])
Out[8]:
[<matplotlib.axis.YTick at 0x28b2d85cac0>,
 <matplotlib.axis.YTick at 0x28b2d85c340>,
 <matplotlib.axis.YTick at 0x28b2d855cd0>,
 <matplotlib.axis.YTick at 0x28b2d899070>,
 <matplotlib.axis.YTick at 0x28b2d899730>,
 <matplotlib.axis.YTick at 0x28b2d899e80>,
 <matplotlib.axis.YTick at 0x28b2d89f610>]
In [9]:
resper1=pd.DataFrame(df[['indice','periodo_op']].groupby('periodo_op').count())
resper2=pd.DataFrame(df[['operation_value','periodo_op']].groupby('periodo_op').sum())
resper=pd.DataFrame(resper1.merge(resper2, left_on='periodo_op', right_on='periodo_op'))
resper
Out[9]:
indice operation_value
periodo_op
202111 212 5283950.0
202112 1265 45911500.0
202201 1422 53801100.0
202202 1446 45420200.0
In [10]:
gper, axs = plt.subplots(1, 2, figsize=(9, 3), sharey=False)
plt.subplots_adjust(left=0.1,bottom=0.1,right=0.9,top=0.8, wspace=0.4, hspace=0.4)
gper.suptitle('Comportamiento por periodo',color='blue', fontsize=15)
axs[0].set_title('Número de Depositos',fontsize=10)
axs[0].plot(resper.index, resper.indice,color="purple")
axs[1].set_title('Valor de Depositos',fontsize=10)
axs[1].plot(resper.index, resper.operation_value,color="purple")
axs[1].set_yticks(ticks=plt.yticks()[0], labels=plt.yticks()[0])
Out[10]:
[<matplotlib.axis.YTick at 0x28b2d918700>,
 <matplotlib.axis.YTick at 0x28b2d910f40>,
 <matplotlib.axis.YTick at 0x28b2d9215e0>,
 <matplotlib.axis.YTick at 0x28b2d943970>,
 <matplotlib.axis.YTick at 0x28b2d9439d0>,
 <matplotlib.axis.YTick at 0x28b2d94c580>,
 <matplotlib.axis.YTick at 0x28b2d94ccd0>]
In [11]:
resfech1=pd.DataFrame(df[['indice','fecha']].groupby('fecha').count())
resfech2=pd.DataFrame(df[['operation_value','fecha']].groupby('fecha').sum())
resfech=pd.DataFrame(resfech1.merge(resfech2, left_on='fecha', right_on='fecha'))
gfech, axs = plt.subplots(1, 2, figsize=(9, 3), sharey=False)
plt.subplots_adjust(left=0.1,bottom=0.1,right=0.9,top=0.8, wspace=0.4, hspace=0.4)
gfech.suptitle('Evolución día a día Depósitos',color='blue', fontsize=15)
axs[0].set_title('Número de Depositos',fontsize=10)
axs[0].tick_params(axis='x', rotation=90)
axs[0].plot(resfech.index, resfech.indice,color="green")
axs[1].set_title('Valor de Depositos',fontsize=10)
axs[1].tick_params(axis='x', rotation=90)
axs[1].plot(resfech.index, resfech.operation_value,color="green")
axs[1].set_yticks(ticks=plt.yticks()[0], labels=plt.yticks()[0])
Out[11]:
[<matplotlib.axis.YTick at 0x28b2d9d0a90>,
 <matplotlib.axis.YTick at 0x28b2d9d0310>,
 <matplotlib.axis.YTick at 0x28b2d9d7280>,
 <matplotlib.axis.YTick at 0x28b2d9f8e50>,
 <matplotlib.axis.YTick at 0x28b2da08550>,
 <matplotlib.axis.YTick at 0x28b2da08ca0>,
 <matplotlib.axis.YTick at 0x28b2da0c430>,
 <matplotlib.axis.YTick at 0x28b2da0cb80>]
In [12]:
resdm1=pd.DataFrame(df[['indice','dia_mes_op','periodo_op']].groupby(['dia_mes_op','periodo_op']).count()).reset_index()
resdm2=pd.DataFrame(df[['operation_value','dia_mes_op','periodo_op']].groupby(['dia_mes_op','periodo_op']).sum()).reset_index()
resdm=pd.DataFrame(resdm1.merge(resdm2, left_on=['dia_mes_op','periodo_op'], right_on=['dia_mes_op','periodo_op']))

plt.title('Comportamiento Número Depósitos por Día Mes')
g1=sns.lineplot(data=resdm, x="dia_mes_op", y="indice", hue="periodo_op")
plt.show()

plt.title('Comportamiento Valor Depósitos por Día Mes')
g2=sns.lineplot(data=resdm, x="dia_mes_op", y="operation_value", hue="periodo_op")
g2.set_yticks(ticks=plt.yticks()[0], labels=plt.yticks()[0])
plt.show()
In [13]:
resds1=pd.DataFrame(df[['indice','dia_semana_op','semana_op_a']].groupby(['dia_semana_op','semana_op_a']).count()).reset_index()
resds2=pd.DataFrame(df[['operation_value','dia_semana_op','semana_op_a']].groupby(['dia_semana_op','semana_op_a']).sum()).reset_index()
resds=pd.DataFrame(resds1.merge(resds2, left_on=['dia_semana_op','semana_op_a'], right_on=['dia_semana_op','semana_op_a']))

fig1 = px.box(resds, x="dia_semana_op", y="indice",title='Comportamiento Número Depósitos por Día de la Semana')
fig1.show()

fig2 = px.box(resds, x="dia_semana_op", y="operation_value",title='Comportamiento Valor Depósitos por Día de la Semana')
fig2.show()
In [14]:
resdh1=pd.DataFrame(df[['indice','hora_op']].groupby(['hora_op']).count()).reset_index()
resdh2=pd.DataFrame(df[['operation_value','hora_op']].groupby(['hora_op']).sum()).reset_index()
resdh=pd.DataFrame(resdh1.merge(resdh2, left_on='hora_op', right_on='hora_op'))

ghd, axs = plt.subplots(1, 2, figsize=(9, 3), sharey=False)
plt.subplots_adjust(left=0.1,bottom=0.1,right=0.9,top=0.8, wspace=0.4, hspace=0.4)
ghd.suptitle('Comportamiento por hora del día',color='blue', fontsize=15)
axs[0].set_title('Número de Depositos',fontsize=10)
axs[0].bar(resdh.hora_op, resdh.indice)
axs[1].set_title('Valor de Depositos',fontsize=10)
axs[1].bar(resdh.hora_op, resdh.operation_value)
axs[1].set_yticks(ticks=plt.yticks()[0], labels=plt.yticks()[0])
Out[14]:
[<matplotlib.axis.YTick at 0x28b2f9003d0>,
 <matplotlib.axis.YTick at 0x28b2f8fbc10>,
 <matplotlib.axis.YTick at 0x28b2f906610>,
 <matplotlib.axis.YTick at 0x28b2f97a400>,
 <matplotlib.axis.YTick at 0x28b2f97ab50>]
In [15]:
#3.4 maplocation_name
res3=pd.DataFrame(df[['indice','maplocation_name']].groupby('maplocation_name').count()).reset_index()
res3=res3.sort_values(by=['indice'],ascending=False)
res4=pd.DataFrame(df[['operation_value','maplocation_name']].groupby('maplocation_name').sum()).reset_index()
res4=res4.sort_values(by=['operation_value'],ascending=False)
res4['par']=round(res4['operation_value']/sum(res4['operation_value'])*100,2)


gloc, axs = plt.subplots(1, 2, figsize=(9, 3), sharey=False)
plt.subplots_adjust(left=0.1,bottom=0.1,right=0.9,top=0.8, wspace=0.4, hspace=0.4)
gloc.suptitle('Comportamiento Depositos por Locación',color='blue', fontsize=15)

axs[0].set_title('Número de Depositos',fontsize=10)
axs[0].bar(res3.maplocation_name, res3.indice)
axs[0].tick_params(axis='x', rotation=90)
axs[1].set_title('Valor de Depositos',fontsize=10)
axs[1].bar(res4.maplocation_name, res4.operation_value)
axs[1].tick_params(axis='x', rotation=90)
axs[1].set_yticks(ticks=plt.yticks()[0], labels=plt.yticks()[0])
res4
Out[15]:
maplocation_name operation_value par
1 CC Plaza de las Américas - Plaza Mariposa 112241000.0 74.62
0 CC Los Molinos - Zona Montaña Nivel 2 34597650.0 23.00
2 Universidad de los Andes - ML Piso 5 3578100.0 2.38

El 74% del valor de los depósitos se ha recibido en el punto del CC Plaza de las Américas

In [16]:
#3.5 user_createddate (fecha_cre_us)
rescu=pd.DataFrame(df[['indice','fecha_cre_us']].groupby('fecha_cre_us').count()).reset_index()
rescu
plt.plot(rescu['fecha_cre_us'], rescu['indice'])
plt.title('Evolución de Creación de Nuevos Usuarios')
plt.xticks(rotation=90)
plt.show()

Planteamiento¶

Dado que se desea evaluar el comportamiento de los diferentes usuarios, se transformará el dataframe para generar las siguientes estadisticas para cada uno de los usuarios:

  1. n_operaciones: número total de depositos realizados
  2. v_operaciones: valor total de depositos realizados
  3. v_prom_op: valor promedio operaciones = v_operaciones/n_operaciones
  4. n_op_prom_mes: número de depositos promedio al mes
  5. v_op_prom_mes: valor promedio de los depositos al mes
  6. lp: lugar preferido para realizar los depósitos
  7. ant_usr_mes: antiguedad del usuario en meses teniendo como referencia el mes de Abr de 2022
  8. ant_usr_a: antiguedad del usuario en años teniendo como referencia el mes de Abr de 2022

A partir de estas variables, se realizará un modelo de clasificación de clientes con el algoritmo K-means

In [403]:
#4.1 Se genera el subtotal de n_operaciones
aux=pd.DataFrame(df[['user_id','indice']].groupby('user_id').count()).reset_index()
aux.rename(columns={'indice':'n_operaciones'},inplace=True)
dfc=aux

#4.2 Se genera el subtotal de v_operaciones
aux=pd.DataFrame(df[['user_id','operation_value']].groupby('user_id').sum()).reset_index()
aux.rename(columns={'operation_value':'v_operaciones'},inplace=True)
dfc=pd.DataFrame(dfc.merge(aux, left_on='user_id', right_on='user_id'))

#4.3 Se genera el valor promedio de las operaciones realizadas: v_prom_op
dfc['v_prom_op']=round(dfc['v_operaciones']/dfc['n_operaciones'],0)

#4.4 Para calcular el numero promedio de operaciones al mes: n_op_prom, se realizará primero el 
# calculo de la antiguedad del cliente ant_usr_mes y ant_usr_a
df['ant_usr_mes']=(2022-pd.DatetimeIndex(df['user_createddate']).year)*12 + 3 - pd.DatetimeIndex(df['user_createddate']).month
aux=pd.DataFrame(df[['user_id','ant_usr_mes']].groupby('user_id').mean()).reset_index()
aux['ant_usr_a']=aux['ant_usr_mes']//12
dfc=pd.DataFrame(dfc.merge(aux, left_on='user_id', right_on='user_id'))

# 4.5 numero promedio de operaciones al mes y valor promedio de los depositos al mes, 
# Se calcula con base en la fecha de creación del usuario
dfc['div']= [4 if x > 4 else x for x in dfc['ant_usr_mes']]
dfc['n_op_prom_mes']=round(dfc['n_operaciones']/dfc['div'],2)
dfc['v_op_prom_mes']=round(dfc['v_operaciones']/dfc['div'],0)

# 4.6 Se obtine lugar preferido para realizar los depósitos a partir del lugar con el numero maximo de depósitos
aux=pd.DataFrame(df[['user_id','maplocation_name','indice']].groupby(['maplocation_name','user_id']).count()).reset_index()
aux2=pd.DataFrame(aux.groupby(['user_id'])['indice'].max()).reset_index()
aux2=pd.DataFrame(aux2.merge(aux, left_on=['user_id','indice'], right_on=['user_id','indice']))
aux2.rename(columns={'maplocation_name':'lp'},inplace=True)
aux2=aux2[['user_id','lp']]
aux3=pd.DataFrame(aux2.groupby(['lp'])['user_id'].count()).reset_index()
# Se representa cada localización prefereida con números: CC Los Molinos = 1 / CC Plaza de las Américas = 2 / 
# Universidad de los Andes = 3
aux3['lp_num']=range(1,4)
aux3=aux3[['lp','lp_num']]
aux2=pd.DataFrame(aux2.merge(aux3, left_on=['lp'], right_on=['lp']))
aux2=aux2[['user_id','lp_num']]
dfc=pd.DataFrame(dfc.merge(aux2, left_on='user_id', right_on='user_id'))
dfcf=dfc
dfcf.drop(['user_id','div'],axis='columns', inplace=True)
dfcf
Out[403]:
n_operaciones v_operaciones v_prom_op ant_usr_mes ant_usr_a n_op_prom_mes v_op_prom_mes lp_num
0 3 44750.0 14917.0 4.0 0.0 0.75 11188.0 2
1 1 259000.0 259000.0 4.0 0.0 0.25 64750.0 1
2 4 375600.0 93900.0 12.0 1.0 1.00 93900.0 1
3 10 433450.0 43345.0 2.0 0.0 5.00 216725.0 1
4 1 1500.0 1500.0 3.0 0.0 0.33 500.0 2
... ... ... ... ... ... ... ... ...
1651 1 50.0 50.0 1.0 0.0 1.00 50.0 1
1652 1 23000.0 23000.0 12.0 1.0 0.25 5750.0 2
1653 1 97800.0 97800.0 14.0 1.0 0.25 24450.0 2
1654 2 2500.0 1250.0 1.0 0.0 2.00 2500.0 1
1655 1 3300.0 3300.0 10.0 0.0 0.25 825.0 1

1656 rows × 8 columns

Revision y eliminación de colinealidad¶

In [404]:
#5 Se revisa la colinealidad de las variables utilizando la matriz de correlación
col=dfcf.corr()
col
Out[404]:
n_operaciones v_operaciones v_prom_op ant_usr_mes ant_usr_a n_op_prom_mes v_op_prom_mes lp_num
n_operaciones 1.000000 0.418901 -0.008806 -0.004857 -0.011666 0.865811 0.369294 -0.019337
v_operaciones 0.418901 1.000000 0.534348 -0.015376 -0.024884 0.345867 0.942932 0.041698
v_prom_op -0.008806 0.534348 1.000000 -0.007882 -0.022588 -0.027263 0.572316 0.081743
ant_usr_mes -0.004857 -0.015376 -0.007882 1.000000 0.936319 -0.175379 -0.069441 0.394011
ant_usr_a -0.011666 -0.024884 -0.022588 0.936319 1.000000 -0.123052 -0.058556 0.366170
n_op_prom_mes 0.865811 0.345867 -0.027263 -0.175379 -0.123052 1.000000 0.365210 -0.033281
v_op_prom_mes 0.369294 0.942932 0.572316 -0.069441 -0.058556 0.365210 1.000000 0.047500
lp_num -0.019337 0.041698 0.081743 0.394011 0.366170 -0.033281 0.047500 1.000000

Conclusión evaluación colinealidad: No se tendran en cuenta en el modelo de clustering las variables con colinealidad mayor a 0.85. De esta forma se tienen en cuenta para el modelo final las variables:

  • v_prom_op
  • ant_usr_a
  • n_op_prom_mes
  • v_op_prom_mes
  • lp_num
  • In [405]:
    #6 Se cargan los paquetes requeridos para agrupar a los clientes en diferentes clases
    from sklearn.preprocessing import StandardScaler
    from scipy.spatial.distance import cdist
    from sklearn.metrics import silhouette_score
    from sklearn.cluster import DBSCAN
    from sklearn.cluster import KMeans
    from sklearn.cluster import AgglomerativeClustering
    
    In [406]:
    #7 Dado que el metodo de agrupación utiliza la distancia entre los datos para clasificarlos, primero se escalan las variables
    #para que todas tengan la misma importancia en el modelo
    dfcsc=dfcf[['v_prom_op','ant_usr_a','n_op_prom_mes','v_op_prom_mes','lp_num']]
    scaler = StandardScaler()
    scaler.fit(dfcsc)
    dfcscsc=scaler.transform(dfcsc)
    
    #8 Se identifica el número optimo de clusters para la agrupación
    
    def dispersion(x, figure_name, max_k = 10, n_init = 10):
        inercia = []
    
        for k in range(1, max_k):
            kmeans = KMeans(n_clusters = k, n_init = n_init).fit(x)
            inercia.append(kmeans.inertia_)
    
        plt.plot(range(1, max_k), inercia, 'bx-')
        plt.xlabel('k')
        plt.ylabel(u'Dispersión')
        plt.title(figure_name)
    dispersion(dfcscsc,'Dispersion Clusters')
    plt.axvline(x=6,linestyle='--',color="lime",label="numero optimo de clusters")
    plt.legend(shadow=True)
    
    Out[406]:
    <matplotlib.legend.Legend at 0x28b371520d0>
    In [409]:
    #9.1 Se realiza la agrupración para 6 clusters
    color_map = np.array(['b','g','r','c','m','y','k','j'])
    kmp = pd.DataFrame(KMeans(n_clusters = 6, random_state = 1).fit_predict(dfcscsc))
    kmp.rename(columns={0:'kmpred'},inplace=True)
    dfcscp=pd.concat([dfcsc, kmp], axis=1)
    rck=pd.DataFrame(dfcscp['kmpred'].value_counts()).reset_index()
    rck['par']=round(rck['kmpred']/sum(rck['kmpred'])*100,2)
    rck.rename(columns={'kmpred':'Cantidad'},inplace=True)
    rck.rename(columns={'index':'kmpred'},inplace=True)
    print(rck)
    
       kmpred  Cantidad    par
    0       0       703  42.45
    1       4       529  31.94
    2       1       242  14.61
    3       2        96   5.80
    4       3        71   4.29
    5       5        15   0.91
    
    In [416]:
    fig, ax = plt.subplots(figsize=(5,5))
    ax = sns.scatterplot(x='n_op_prom_mes',y='v_op_prom_mes',hue = 'kmpred',data = dfcscp,legend='full',
                        palette = {0:'red',1:'orange',2:'lightgreen',3:'lime',4:'gray',5:'green'})
    ax.legend(loc='upper left')
    
    Out[416]:
    <matplotlib.legend.Legend at 0x28b2dcd0eb0>
    In [415]:
    resag=pd.DataFrame(dfcscp[['v_prom_op','ant_usr_a','n_op_prom_mes','v_op_prom_mes','lp_num','kmpred']].groupby('kmpred').mean()).reset_index()
    resag=pd.DataFrame(resag.merge(rck, left_on=['kmpred'], right_on=['kmpred']))
    resag=resag.sort_values(by=['v_op_prom_mes'],ascending=False)
    resag
    
    Out[415]:
    kmpred v_prom_op ant_usr_a n_op_prom_mes v_op_prom_mes lp_num Cantidad par
    5 5 496432.066667 0.133333 3.516667 899725.266667 2.000000 15 0.91
    2 2 249055.020833 0.145833 0.713229 146488.541667 1.906250 96 5.80
    3 3 16014.070423 0.056338 5.504789 79090.394366 1.521127 71 4.29
    4 4 21155.396975 0.000000 0.816314 16021.930057 2.234405 529 31.94
    0 0 13717.083926 0.116643 0.772290 11689.133713 1.000000 703 42.45
    1 1 23813.814050 1.557851 0.538223 11358.735537 2.466942 242 14.61

    Conclusion valoración de usuarios¶

    En la base de datos de transacciones se identifican tres tipos de usuarios:

    1. Usuarios Alto Deposito: Usuarios con alto valor y frecuencia de depósitos, identificados en la gráfica anterior con diferentes tonos de verde (clusters: 5, 2 y 3) se caracterizan por contar con depósitos altos cada mes y alta frecuencia de operaciones al mes, por desgracia solo son el 10% de los usuarios
    2. Nuevos usuarios bajo Deposito: Usuarios nuevos con bajos depósitos, corresponden al 32% y estan idenficados con color gris
    3. Usuarios bajo deposito: Usuarios con baja frecuencia y bajo valor de depositos corresponde al 57% y estan identificados con color rojo y naranja

    De acuerdo con lo anterior, se evaluan los usuarios de coink como buenos, si se clasifican dentro de los cluster 5, 2 y 3, regulares, si estan dentro del cluster 4 y malos si esta dentro de los cluster 0 y 1

    Dado los costos de vinculación y administración de cada usuario, se espera del área de marketing estrategias para incrementar mes a mes el número y calidad de los usuarios. Está metrica podria verse así:

    In [446]:
    usbuenos= [random.randint((160 + x*30), (160 + x*70)) for x in range(1,13)]
    usregulares= [random.randint((480 + x*10), (480 + x*15)) for x in range(1,13)]
    usmalos = [random.randint((960 - (x*30)), (960 - (x*20))) for x in range(1,13)]
    periodos =['202203','202204','202205','202206','202207','202208','202209','202210','202211','202212','202301','202302']
    print(usmalos)
    print(usregulares)
    print(usbuenos)
    plt.stackplot(periodos,usbuenos, usregulares,usmalos,labels = ["Buenos", "Regulares", "Malos"],colors=['green','gray','orange'])
    plt.legend(loc = 'upper left')
    plt.title('Evolución de Creación de Nuevos Usuarios')
    plt.xticks(rotation=90)
    plt.show()
    
    [930, 917, 892, 846, 812, 813, 776, 722, 696, 722, 702, 611]
    [490, 506, 520, 526, 542, 545, 550, 576, 609, 628, 644, 601]
    [206, 263, 264, 365, 423, 377, 548, 585, 719, 513, 562, 618]
    

    Punto 2¶

    Tal como lo menciona el Blog del enlace y los manuales en la aplicación, el procedimiento sería:

    1. Abrir Studio Classic desde Amazon Sagemaker Studio
    2. Crear un notebook, crear y entrenar el modelo
    3. Especificar los contenedores para los datos de entrenamiento y el modelo
    4. Lanzar el modelo en Sagemaker
    5. Configurar el hosting del modelo
    6. Configurar y crear el Endpoint
    7. Con el endpoint configurado, se puede llamar el modelo desde una API. Tambien se puede probar desde Studio Classic utilizando la opción: runtime.invoke_endpoint

    Punto 3¶

    1. Análisis exploratorio:¶

    Se identifican y seleccionan las variables que inciden de manera significativa en la satisfacción del trabajador

    In [129]:
    from scipy.stats import chi2_contingency
    from scipy.stats import kruskal
    
    In [30]:
    #1 comportamiento variable dependiente
    dfs = pd.read_excel('D:/Mega/Personal/Varios/Procesos Seleccion/Coink/info_satisfaccion_trabajo.xlsx')
    dfs.describe()
    dfs.head()
    dfs['JobSatisfaction'].describe()
    dfs['JobSatisfaction'].hist()
    plt.title('Histograma de frecuencia JobSatisfaction')
    
    Out[30]:
    Text(0.5, 1.0, 'Histograma de frecuencia JobSatisfaction')

    Para los datos categoricos se validará la incidencia de la variable independiente sobre la variable JobSatisfaction usando la tabla de contingencia de frecuencias y la prueba Chi-cuadrado

    Para los datos continuos grandes enteros o reales, se validará la incidencia sobre la variable dependiente con el test de Kruskal-Wallis

    Los datos tipo texto con mas de 10 categorias seran eliminados del modelo

    In [70]:
    #1 Revisión de columnas tipo texto: se aplica la prueba chi2 para cada una de las variables de texto con respecto
    # a JobSatisfaction
    lvi = pd.DataFrame(dfs.dtypes).reset_index()
    lvt = lvi[lvi[0] == 'object']
    lvtlv = lvt['index'].tolist()
    resvt = pd.DataFrame(columns=['variable', 'decision', 'valor p','numero de categorias'])
    i=0
    for v in lvtlv:
        dfa = dfs[[v,'JobSatisfaction']]
        tc = pd.crosstab(index=dfa[v],columns=dfa['JobSatisfaction'], margins=True)
        ph = chi2_contingency(tc)[1]
        resvt.loc[i,'variable']=v
        if ph < 0.05:
            resvt.loc[i,'decision']='incluir'
        else:
            resvt.loc[i,'decision']='excluir'
        resvt.loc[i,'valor p']=ph
        resvt.loc[i,'numero de categorias']=len(tc)-1
        i=i+1
    resvt    
    
    Out[70]:
    variable decision valor p numero de categorias
    0 Attrition incluir 0.025259 2
    1 BusinessTravel excluir 0.950231 3
    2 Department excluir 0.742606 3
    3 EducationField excluir 0.877563 6
    4 Gender excluir 0.95947 2
    5 JobRole excluir 0.993416 9
    6 MaritalStatus excluir 0.996563 3
    7 Over18 excluir 1.0 1
    8 OverTime excluir 0.884112 2

    Conclusion:¶

    De las variables tipo texto solo se incluirá en el modelo la variable Attrition

    In [126]:
    tc = pd.crosstab(columns=dfs['Attrition'],index=dfs['JobSatisfaction'], margins=False, normalize='index')
    barplot = tc.plot.bar(rot=0,color=['b','gray'],stacked=True)
    
    In [ ]:
    #2 Revisión de columnas tipo  numero: se aplica la prueba chi2 para las variables numericas con menos de 10 valores, dado que 
    # estan representando variables categoricas
    # para las variables numericas con un rango mayor de 10, se aplica la pruebaa Kruskal-Wallis
    
    In [118]:
    dvn=dfs.describe().transpose()
    dvn['rango']=dvn['max']-dvn['min']
    dvn['eval_chi2']= ['si' if x < 11 else 'no' for x in dvn['rango']]
    dvn
    
    Out[118]:
    count mean std min 25% 50% 75% max rango eval_chi2
    Age 1470.0 36.923810 9.135373 18.0 30.00 36.0 43.00 60.0 42.0 no
    DailyRate 1470.0 802.485714 403.509100 102.0 465.00 802.0 1157.00 1499.0 1397.0 no
    DistanceFromHome 1470.0 9.192517 8.106864 1.0 2.00 7.0 14.00 29.0 28.0 no
    Education 1470.0 2.912925 1.024165 1.0 2.00 3.0 4.00 5.0 4.0 si
    EmployeeCount 1470.0 1.000000 0.000000 1.0 1.00 1.0 1.00 1.0 0.0 si
    EmployeeNumber 1470.0 1024.865306 602.024335 1.0 491.25 1020.5 1555.75 2068.0 2067.0 no
    EnvironmentSatisfaction 1470.0 2.721769 1.093082 1.0 2.00 3.0 4.00 4.0 3.0 si
    HourlyRate 1470.0 65.891156 20.329428 30.0 48.00 66.0 83.75 100.0 70.0 no
    JobInvolvement 1470.0 2.729932 0.711561 1.0 2.00 3.0 3.00 4.0 3.0 si
    JobLevel 1470.0 2.063946 1.106940 1.0 1.00 2.0 3.00 5.0 4.0 si
    JobSatisfaction 1470.0 2.728571 1.102846 1.0 2.00 3.0 4.00 4.0 3.0 si
    MonthlyIncome 1470.0 6502.931293 4707.956783 1009.0 2911.00 4919.0 8379.00 19999.0 18990.0 no
    MonthlyRate 1470.0 14313.103401 7117.786044 2094.0 8047.00 14235.5 20461.50 26999.0 24905.0 no
    NumCompaniesWorked 1470.0 2.693197 2.498009 0.0 1.00 2.0 4.00 9.0 9.0 si
    PercentSalaryHike 1470.0 15.209524 3.659938 11.0 12.00 14.0 18.00 25.0 14.0 no
    PerformanceRating 1470.0 3.153741 0.360824 3.0 3.00 3.0 3.00 4.0 1.0 si
    RelationshipSatisfaction 1470.0 2.712245 1.081209 1.0 2.00 3.0 4.00 4.0 3.0 si
    StandardHours 1470.0 80.000000 0.000000 80.0 80.00 80.0 80.00 80.0 0.0 si
    StockOptionLevel 1470.0 0.793878 0.852077 0.0 0.00 1.0 1.00 3.0 3.0 si
    TotalWorkingYears 1470.0 11.279592 7.780782 0.0 6.00 10.0 15.00 40.0 40.0 no
    TrainingTimesLastYear 1470.0 2.799320 1.289271 0.0 2.00 3.0 3.00 6.0 6.0 si
    WorkLifeBalance 1470.0 2.761224 0.706476 1.0 2.00 3.0 3.00 4.0 3.0 si
    YearsAtCompany 1470.0 7.008163 6.126525 0.0 3.00 5.0 9.00 40.0 40.0 no
    YearsInCurrentRole 1470.0 4.229252 3.623137 0.0 2.00 3.0 7.00 18.0 18.0 no
    YearsSinceLastPromotion 1470.0 2.187755 3.222430 0.0 0.00 1.0 3.00 15.0 15.0 no
    YearsWithCurrManager 1470.0 4.123129 3.568136 0.0 2.00 3.0 7.00 17.0 17.0 no
    In [119]:
    lvnc = dvn[(dvn['eval_chi2'] == 'si') & (dvn['rango']>0)]
    lvncv =lvnc.index.tolist()
    lvncv.remove('JobSatisfaction')
    resvnc = pd.DataFrame(columns=['variable', 'decision', 'valor p','numero de categorias'])
    i=0
    for v in lvncv:
        dfa = dfs[[v,'JobSatisfaction']]
        tc = pd.crosstab(index=dfa[v],columns=dfa['JobSatisfaction'], margins=True)
        ph = chi2_contingency(tc)[1]
        resvnc.loc[i,'variable']=v
        if ph < 0.05:
            resvnc.loc[i,'decision']='incluir'
        else:
            resvnc.loc[i,'decision']='excluir'
        resvnc.loc[i,'valor p']=ph
        resvnc.loc[i,'numero de categorias']=len(tc)-1
        i=i+1
    resvnc 
    
    Out[119]:
    variable decision valor p numero de categorias
    0 Education excluir 0.876106 5
    1 EnvironmentSatisfaction excluir 0.993924 4
    2 JobInvolvement excluir 0.964259 4
    3 JobLevel excluir 0.998507 5
    4 NumCompaniesWorked excluir 0.985831 10
    5 PerformanceRating excluir 0.855829 2
    6 RelationshipSatisfaction excluir 0.999844 4
    7 StockOptionLevel excluir 0.999904 4
    8 TrainingTimesLastYear excluir 0.866542 7
    9 WorkLifeBalance excluir 0.980587 4

    Conclusión variables numericas enteras pequeñas:¶

    Se excluyen todas las variables numericas enteras dado que ninguna afecta significativamente el JobSatisfaction

    In [127]:
    tc = pd.crosstab(columns=dfs['PerformanceRating'],index=dfs['JobSatisfaction'], margins=False, normalize='index')
    barplot = tc.plot.bar(rot=0,color=['b','gray'],stacked=True)
    
    In [143]:
    lvne = dvn[dvn['eval_chi2'] == 'no']
    lvnev =lvne.index.tolist()
    resvne = pd.DataFrame(columns=['variable', 'decision', 'valor p'])
    i=0
    for v in lvnev:
        dfa = dfs[[v,'JobSatisfaction']]
        g1=dfa[dfa['JobSatisfaction'] == 1][v]
        g2=dfa[dfa['JobSatisfaction'] == 2][v]
        g3=dfa[dfa['JobSatisfaction'] == 3][v]
        g4=dfa[dfa['JobSatisfaction'] == 4][v]
        ph= kruskal(g1,g2,g3,g4)[1]
        resvne.loc[i,'variable']=v
        if ph < 0.05:
            resvne.loc[i,'decision']='incluir'
        else:
            resvne.loc[i,'decision']='excluir'
        resvne.loc[i,'valor p']=ph
        i=i+1
    resvne
    
    Out[143]:
    variable decision valor p
    0 Age excluir 0.9564
    1 DailyRate excluir 0.149262
    2 DistanceFromHome excluir 0.69568
    3 EmployeeNumber excluir 0.331907
    4 HourlyRate incluir 0.039766
    5 MonthlyIncome excluir 0.858179
    6 MonthlyRate excluir 0.676142
    7 PercentSalaryHike excluir 0.427498
    8 TotalWorkingYears excluir 0.657358
    9 YearsAtCompany excluir 0.930758
    10 YearsInCurrentRole excluir 0.995135
    11 YearsSinceLastPromotion excluir 0.916826
    12 YearsWithCurrManager excluir 0.727609

    Conclusión variables númericas rango alto:¶

    Solo se considerará en el modelo la variable HourlyRate, la cual es la única que incide significativamente en la satisfaccion del empleado

    In [189]:
    g1=pd.DataFrame(dfs[dfs['JobSatisfaction'] == 1]['HourlyRate']).reset_index()
    g2=pd.DataFrame(dfs[dfs['JobSatisfaction'] == 2]['HourlyRate']).reset_index()
    g3=pd.DataFrame(dfs[dfs['JobSatisfaction'] == 3]['HourlyRate']).reset_index()
    g4=pd.DataFrame(dfs[dfs['JobSatisfaction'] == 4]['HourlyRate']).reset_index()
    
    fig, ((ax0, ax1), (ax2, ax3)) = plt.subplots(nrows=2, ncols=2,)
    plt.subplots_adjust(left=0.1,bottom=0.1,right=0.9,top=0.8, wspace=0.4, hspace=0.4)
    fig.suptitle('Histogramas JobSatisfaction vs HourlyRate',color='blue', fontsize=15)
    
    
    axs[1].set_title('Valor de Depositos',fontsize=10)
    ax0.hist(g1.HourlyRate, 10, density=True, histtype='bar', color='red')
    ax0.set_title('JobSatisfaction = 1',fontsize=10)
    ax1.hist(g2.HourlyRate, 10, density=True, histtype='bar', color='blue')
    ax1.set_title('JobSatisfaction = 2',fontsize=10)
    ax2.hist(g3.HourlyRate, 10, density=True, histtype='bar', color='lime')
    ax2.set_title('JobSatisfaction = 3',fontsize=10)
    ax3.hist(g4.HourlyRate, 10, density=True, histtype='bar', color='green')
    ax3.set_title('JobSatisfaction = 4',fontsize=10)
    
    Out[189]:
    Text(0.5, 1.0, 'JobSatisfaction = 4')

    2. Modelos:¶

    Se realizan los modelos 1. Knn y 2. Arbol de Decisión. Para el modelo knn se remplazan los valores de Attrition (yes,no) por 1 y 0, dado que ese modelo clasifica según distancias

    In [363]:
    from sklearn.model_selection import train_test_split
    from sklearn.neighbors import KNeighborsClassifier
    from sklearn.metrics import confusion_matrix
    from sklearn.metrics import classification_report
    from sklearn.ensemble import RandomForestClassifier
    from numpy import *
    
    In [320]:
    # 1. Se seleccionan las columnas y se divide el dataframe en subconjunto de entrenamiento y prueba
    dfs['Attrition_aj']=[1 if x == 'Yes' else 0 for x in dfs['Attrition']]
    x = dfs[['Attrition_aj','HourlyRate']]
    scaler = StandardScaler()
    scaler.fit(x)
    xs=scaler.transform(x)
    
    y = dfs[['JobSatisfaction']]
    xs_train, xs_test, y_train, y_test = train_test_split(xs, y,random_state=29)
    
    In [325]:
    #2 Entrenamiento del modelo y evaluación
    knn = KNeighborsClassifier(n_neighbors=5)
    knn.fit(xs_train,y_train)
    y_pred=knn.predict(xs_test)
    cf=classification_report(y_test,y_pred)
    print(cf)
    
                  precision    recall  f1-score   support
    
               1       0.22      0.22      0.22        63
               2       0.22      0.23      0.22        71
               3       0.31      0.41      0.35       109
               4       0.38      0.26      0.31       125
    
        accuracy                           0.29       368
       macro avg       0.28      0.28      0.27       368
    weighted avg       0.30      0.29      0.29       368
    
    
    D:\anaconda3\lib\site-packages\sklearn\neighbors\_classification.py:198: DataConversionWarning:
    
    A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples,), for example using ravel().
    
    D:\anaconda3\lib\site-packages\sklearn\neighbors\_classification.py:228: FutureWarning:
    
    Unlike other reduction functions (e.g. `skew`, `kurtosis`), the default behavior of `mode` typically preserves the axis it acts along. In SciPy 1.11.0, this behavior will change: the default value of `keepdims` will become False, the `axis` over which the statistic is taken will be eliminated, and the value None will no longer be accepted. Set `keepdims` to True or False to avoid this warning.
    
    

    Conclusión predicción KNN:¶

    El modelo cuenta con una precisión del 30%

    In [392]:
    #3 Modelo ramdom forest
    
    x = dfs[['Attrition_aj','HourlyRate']]
    y = dfs[['JobSatisfaction']]
    x_train, x_test, y_train, y_test = train_test_split(x, y,random_state=86)
    rf=RandomForestClassifier(n_estimators=20,random_state = 123)
    rf.fit(x_train,y_train)
    y_pred=rf.predict(x_test)
    cf=classification_report(y_test,y_pred)
    print(cf)
    
                  precision    recall  f1-score   support
    
               1       0.26      0.16      0.20        63
               2       0.23      0.10      0.14        71
               3       0.37      0.50      0.42       111
               4       0.41      0.50      0.45       123
    
        accuracy                           0.36       368
       macro avg       0.32      0.31      0.30       368
    weighted avg       0.34      0.36      0.34       368
    
    
    C:\Users\macke\AppData\Local\Temp\ipykernel_18436\1101891941.py:7: DataConversionWarning:
    
    A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples,), for example using ravel().
    
    

    Conclusión predicción Random Forest:¶

    El modelo cuenta con una precisión del 34%

    Monitoreo del modelo en producción¶

    Para monitorear el modelo en producción, se deben comparar los resultados reales del JobSatisfaction vs los resultados que predice el modelo para obtener las medidas de precisión y recuperación.

    Si en la validación de los resultados reales vs los predichos por el modelo, el valor de la preción o recuperación bajan de forma significativa, se debe evaluar ajustar o entrenar el modelo nuevamente.

    No obstante, dada la baja precisión de los dos modelos realizados, deberia considerarse la identificación de otras variables significativamente incidentes en la satisfaccion del trabajador o cambiar la escala de valoración de 1 a 10

    In [ ]: